/+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 + SoulFind - Free SoulSeek server                                           +
 +                                                                           +
 + Copyright (C) 2005 SeeSchloss <seeschloss@seeschloss.org>                 +
 +                                                                           +
 + This  program  is free software ; you can  redistribute it  and/or modify +
 + it under  the  terms of  the GNU General Public License  as published  by +
 + the  Free  Software  Foundation ;  either  version  2 of  the License, or +
 + (at your option) any later version.                                       +
 +                                                                           +
 + This  program  is  distributed  in the  hope  that  it  will  be  useful, +
 + but   WITHOUT  ANY  WARRANTY ;  without  even  the  implied  warranty  of +
 + MERCHANTABILITY   or   FITNESS   FOR   A   PARTICULAR  PURPOSE.  See  the +
 + GNU General Public License for more details.                              +
 +                                                                           +
 + You  should  have  received  a  copy  of  the  GNU General Public License +
 + along   with  this  program ;  if  not,  write   to   the  Free  Software +
 + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA +
 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++/


module server;

import defines;

private import client;
private import messages, message_codes;
private import db;
private import room;
private import pm;

import std.stdio;
import std.socket, std.socketstream;
import std.conv;
import std.c.process;
import std.c.time;
import std.thread;
import std.file;
import std.utf;
import std.date;
import std.md5;		// to encode passwords
import std.string;

version (linux)
	{ // for SIGPIPE
	version (DigitalMars) import std.c.linux.linux;
	else                  import std.c.unix.unix;
	}


void help (char[][] args)
	{
	writefln ("Usage: %s [database_file] [-d|--deamon]", args[0]);
	writefln ("\tdatabase_file: path to the sqlite3 database (default: %s)", default_db_file);
	writefln ("\t-d, --deamon : fork in the background");
	exit (0);
	}

void main (char[][] args)
	{
	char[] db;
	
	bool deamon = false;

	if (args.length > 3) help (args);

	foreach (char[] arg ; args[1 .. length])
		{
		switch (arg)
			{
			case "-h":
			case "--help":
				help (args);
				break;
			case "-d":
			case "--deamon":
				deamon = true;
				break;
			default:
				db = arg;
				break;
			}
		}
	
	if (db.length == 0) db = default_db_file;

	if (deamon)
		{
		version (linux)
			{
			if (fork ()) exit (0);
			}
		else
			{
			writefln ("--deamon: only supported under Linux");
			}
		}
	
	Server s = new Server (db);
	s.listen ();

	if (!deamon) writefln ("Exiting.");
	}

class Server
	{
	uint port;
	uint max_users;
	char[] motd;
	char[] server_user;

	uint max_message_size;
	uint max_offline_pms;

	long started_at;	// for server uptime

	int timeoutval = 240*1000000; // 2 minutes (µseconds)
	Sdb db; // users database

	Socket serverSocket;
	User[Socket] user_sockets;

	this (char[] db_file)
		{
		this.started_at = getUTCtime ();
		db = new Sdb (db_file);
	
		config ();

		version (linux)
			{
			sigaction_t sigp;
			sigp.sa_handler = &sigpipe;
			sigaction (SIGPIPE, &sigp, null);
			}
		}
	
	void listen ()
		{
		Socket socket = new TcpSocket ();
		try
			{
			socket.setOption (SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, 1);
			}
		catch (Exception e)
			{
			writefln ("Unable to set socket option REUSEADDR.");
			}
		try
			{
			socket.bind (new InternetAddress (port));
			socket.listen (10);
			}
		catch (Exception e)
			{
			writef ("Unable to bind socket to port %d", port);
			if (port < 1024)
				writefln (", could it be that you're trying to use a port less than 1024 while running as a user ?");
			else
				writefln ();
			exit (1789);
			}
		writefln ("%d:%d", getpid (), port);

		SocketSet sockset = new SocketSet (max_users + 1);
		
		
		while (true)
			{
			sockset.reset ();
			sockset.add (socket);
			foreach (Socket s ; user_sockets.keys) sockset.add (s);
			uint nb = Socket.select (sockset, null, null, timeoutval);
			if (nb == 0)
				{
				send_pings ();
				}
			if (sockset.isSet (socket))
				{
				nb--;
				debug (3) writefln ("Waiting for a connection...");
				Socket sock = socket.accept ();
				debug (3)
					{
					try {writefln ("Connection accepted from ", sock.remoteAddress().toString());}
					catch (Exception e) {writefln ("?");}
					}
				User user = new User (this, sock, (cast (InternetAddress) sock.remoteAddress()).addr());
				user_sockets[sock] = user;
				sockset.remove (socket);
				}
			foreach (Socket s ; user_sockets.keys)
				{
				if (nb == 0) break;
				if (s !is null && sockset.isSet (s))
					{
					nb--;
					if (!user_sockets[s].recv_message ())
						{
						user_sockets[s].exit ();
						sockset.remove (s);
						this.del_user (user_sockets[s]);
						user_sockets.remove (s);
						s.close ();
						}
					}
				}
			}
		}

	// Filesearches
	void do_FileSearch (uint token, char[] string, char[] username)
		{					// user who sends the search
		Message m = new SFileSearch (username, token, string);
		this.send_to_all (m);
		}

	void do_UserSearch (uint token, char[] string, char[] username,                char[] to)
		{					// user who sends the search	// to this user
		Message m = new SUserSearch (username, token, string);
		User u = get_user (to);

		if (u is null)
			{
			return;
			}
		else
			{
			u.send_message (m);
			}
		}
	
	void do_RoomSearch (uint token, char[] string, char[] username, char[] room)
		{
		Message m = new SRoomSearch (username, token, string);

		Room r = Room.get_room (room);

		if (r is null)
			{
			return;
			}
		else
			{
			r.send_to_all (m);
			}
		}

	// Users
	private User[char[]] user_list;
	private char[][char[]] passwords;
	uint nb_users ()
		{
		return user_list.length;
		}
	
	char[][] user_names ()
		{
		return user_list.keys;
		}
	
	User[] users ()
		{
		return user_list.values;
		}
	
	bit find_user (User user) {return find_user (user.username);}
	bit find_user (char[] username)
		{
		return (username in user_list) ? true : false;
		}
	
	User get_user (char[] username)
		{
		if (find_user (username))
			{
			return user_list[username];
			}
		else
			{
			return null;
			}
		}

	void add_user (User user)
		{
		user_list[user.username] = user;
		}

	void del_user (User user)
		{
		if (user.socket in user_sockets) user_sockets.remove (user.socket);
		if (find_user (user)) user_list.remove (user.username);
		}

	void send_pings ()
		{
		foreach (User u ; users ())
			{
			if ((getUTCtime() - u.last_message_date) >= (timeoutval/1000 * TicksPerSecond))
				{
				u.send_message (new SServerPing ());
				}
			}
		}
	
	void send_to_all (Message m)
		{
		debug (2) writef ("Sending message (", blue,  message_name[m.code], black, " - code ", blue, m.code, black, ") to all users");
		foreach (User u ; users ())
			{
			debug (2) writef (".");
			u.send_message (m);
			}
		debug (2) writefln ();
		}
	
	// recommendations
	uint[char[]] global_recommendations ()
		{
		uint[char[]] list;
		
		foreach (User u ; this.users ())
			{
			foreach (char[] thing ; u.things_he_likes)
				{
				list[thing]++;
				}
			}

		return list;
		}

	// admin
	char[][char[]]	admins;
	
	void del_admin (char[] name)
		{
		if (name in admins) admins.remove (name);
		this.db.del_admin (name);
		}

	void add_admin (char[] name)
		{
		admins[name] = name;
		this.db.add_admin (name);
		
		if (find_user (name)) get_user (name).admin = true;
		}

	void admin_message (User admin, char[] message)
		{
		char[][] command = split (message);
		if (command.length > 0) switch (command[0])
			{
			case "help":
				//this.adminpm (admin, "nbusers, users, info <user>, killall, kill <user>, [un]ban <user>, (add|del)admin <user>, admins, rooms, addprivileges <days> <user>, message <message>, uptime, reload");
				this.adminpm (admin, "Available commands :\n\n"
						     "nbusers\n\tNumber of users connected\n\n"
				                     "users\n\tInfo about each connected user\n\n"
						     "info <user>\n\tInfo about user <user>\n\n"
						     "killall\n\tDisconnect all users\n\n"
						     "kill <user>\n\tDisconnect <user>\n\n"
						     "[un]ban <user>\n\tUnban or ban and disconnect user <user>\n\n"
						     "(add|del)admin <user>\n\tMake <user> an admin\n\n"
						     "admins\n\tList admins\n\n"
						     "rooms\n\tList rooms and number of occupiants\n\n"
						     "addprivileges <days> <user>\n\tAdd <days> days of privileges to user <user>\n\n"
						     "message <message>\n\tSend global message <message> (Note: Museeq users will not see it)\n\n"
						     "uptime\n\tShow server uptime\n\n"
						     "reload\n\tReload settings (Admins, MOTD, max sixes, etc)");
				break;
			case "addprivileges":
				uint days;
				if (command.length < 3)
					{
					this.adminpm (admin, "Syntax is : addprivileges <days> <user>");
					break;
					}
				try
					{
					days =  toInt (command[1]);
					}
				catch (Exception e)
					{
					this.adminpm (admin, "Badly formatted number.");
					break;
					}
				char[] user = join (command[2 .. length], " ");
				if (this.find_user (user))
					{
					this.get_user (user).add_privileges (days*3600*24);
					}
				else
					{
					this.adminpm (admin, format ("User %s does not exist.", user));
					}
				break;
			case "nbusers":
				this.adminpm (admin, format ("%d connected users.", this.nb_users ()));
				break;
			case "users":
				this.adminpm (admin, this.show_users());
				break;
			case "info":
				if (command.length < 2)
					{
					this.adminpm (admin, "Syntax is : info <user>");
					break;
					}
				this.adminpm (admin, this.show_user (join (command[1 .. length], " ")));
				break;
			case "killall":
				debug (1) writefln ("Admin request to kill ALL users...");
				this.kill_all_users ();
				break;
			case "kill":
				if (command.length < 2)
					{
					this.adminpm (admin, "Syntax is : kill <user>");
					break;
					}
				this.kill_user (join (command[1 .. length], " "));
				this.adminpm (admin, format ("User %s kicked from the server", join (command[1 .. length], " ")));
				break;
			case "ban":
				if (command.length < 2)
					{
					this.adminpm (admin, "Syntax is : ban <user>");
					break;
					}
				this.ban_user (join (command[1 .. length], " "));
				this.adminpm (admin, format ("User %s banned from the server", join (command[1 .. length], " ")));
				break;
			case "unban":
				if (command.length < 2)
					{
					this.adminpm (admin, "Syntax is : unban <user>");
					break;
					}
				this.unban_user (join (command[1 .. length], " "));
				this.adminpm (admin, format ("User %s not banned anymore", join (command[1 .. length], " ")));
				break;
			case "addadmin":
				if (command.length < 2)
					{
					this.adminpm (admin, "Syntax is : addadmin <user>");
					break;
					}
				this.add_admin (join (command[1 .. length], " "));
				break;
			case "deladmin":
				if (command.length < 2)
					{
					this.adminpm (admin, "Syntax is : deladmin <user>");
					break;
					}
				this.del_admin (join (command[1 .. length], " "));
				break;
			case "admins":
				char[] list;
				foreach (char[] s ; this.admins)
					{
					list ~= s ~ " ";
					}
				this.adminpm (admin, list);
				break;
			case "rooms":
				char[] list;
				foreach (Room r ; Room.rooms ())
					{
					list ~= format ("%s:%d ", r.name, r.nb_users ());
					}
				this.adminpm (admin, list);
				break;
			case "message":
				if (command.length < 2)
					{
					this.adminpm (admin, "Syntax is : message <message>");
					break;
					}
				this.global_message (join (command[1 .. length], " "));
				break;
			case "uptime":
				this.adminpm (admin, print_length (uptime ()));
				break;
			case "reload":
				this.config (true);
				this.adminpm (admin, "Configuration and admins list reloaded");
				break;
			default:
				this.adminpm (admin, "Don't expect me to understand what you want if you don't use a correct command...");
				break;
			}
		}
	
	void adminpm (User admin, char[] message)
		{
		PM pm = new PM (message, this.server_user, admin.username);
		admin.send_pm (pm);
		}
	
	void global_message (char[] message)
		{
		foreach (User user ; user_list)
			{
			user.send_message (new SAdminMessage (message));
			}
		}
	
	char[] show_users ()
		{
		char[] s;
		foreach (char[] username ; this.user_names ())
			{
			s ~= this.show_user (username) ~ "\n";
			}
		return s;
		}
	
	char[] show_user (char[] username)
		{
		if (this.find_user (username))
			{
			User user = this.get_user (username);
			return format (user.username, " : connected at ", std.date.toString (user.connected_at),
						  "\n\tclient version :", user.cversion,
						  "\n\taddress : ", (cast (SocketStream) user.stream).socket.remoteAddress().toString(),
						  "\n\tadmin : ", user.admin,
						  "\n\tfiles : ", user.shared_files,
						  "\n\tdirs : ", user.shared_folders,
						  "\n\tstatus : ", user.status,
						  "\n\tprivileges : ", user.print_privileges (),
						  "\n\tjoined rooms : ", user.list_joined_rooms ());
			}
		else return "";
		}

	void kill_all_users () 
		{
		foreach (User user ; this.user_list)
			{
			user.exit ();
			}
		}
	
	void kill_user (char[] user)
		{
		if (this.find_user (user))
			{
			get_user (user).exit ();
			}
		}

	void ban_user (char[] user)
		{
		if (this.db.user_exists (user))
			{
			db.user_update_field (user, "banned", 1);
			get_user (user).exit ();
			}
		}

	void unban_user (char[] user)
		{
		if (this.db.user_exists (user))
			{
			db.user_update_field (user, "banned", 0);
			}
		}

	char[] get_motd (char[] name, int vers)
		{
		char[] ret;
		ret = replace (this.motd, "%version%", VERSION);
		ret = replace (ret, "%nbusers%", std.string.toString (this.nb_users ()));
		ret = replace (ret, "%username%", name);
		ret = replace (ret, "%userversion%", std.string.toString (vers));
		return ret;
		}

	// config
	void config (bool reload = false)
		{
		if (!reload) this.port		= db.conf_get_int ("port");
		if (!reload) this.max_users	= db.conf_get_int ("max_users");
		this.max_message_size		= db.conf_get_int ("max_message_size");
		this.max_offline_pms		= db.conf_get_int ("max_offline_pms");
		this.motd			= db.conf_get_str ("motd");
		this.server_user		= db.conf_get_str ("server_user");

		foreach (char[] admin ; db.get_admins ())
			{
			this.admins[admin] = admin;
			}
		}
	
	long uptime ()	// returns uptime, in seconds
		{
		long time = (getUTCtime () - this.started_at) / TicksPerSecond;
		return time;
		}
	
	char[] print_uptime ()
		{
		return print_length (uptime);
		}
	

	char[] encode_password (char[] pass)
		{
		ubyte[16] digest;
		sum (digest, pass);
		char[] s;
		foreach (ubyte u ; digest)
			{
			s ~= format ("%02x", u);
			}
		return s;
		}
	
	bool check_string (char[] string)
		{
		try
			{
			validate (string);
			}
		catch (UtfError)
			{
			return false;
			}

		dchar[] forbidden = \u0000 \u0001 \u0002 \u0003 \u0004 \u0005
		      \u0006 \u0007 \u0008 \u0009 \u000A \u000B \u000D \u000E
		      \u000F \u0010 \u0011 \u0012 \u0013 \u0014 \u0015 \u0016
		      \u0017 \u0018 \u0019 \u001A \u001B \u001C \u001D \u001E
		      \u001F \u007F \u0080 \u0081 \u0082 \u0083 \u0084 \u0085
		      \u0086 \u0087 \u0088 \u0089 \u008A \u008B \u008C \u008D
		      \u008E \u008F \u0090 \u0091 \u0092 \u0093 \u0094 \u0095
		      \u0096 \u0097 \u0098 \u0099 \u009A \u009B \u009C \u009D
		      \u009E \u009F \u00A0 \u00AD
		      // some control chars
		      
		      \u2000 \u2001 \u2002 \u2003 \u2004 \u2005 \u2006 \u2007
		      \u2008 \u2009 \u200A \u200B \u200C \u200D \u200E \u200F;
		      // separators, joiners, etc

		foreach (dchar c ; forbidden)
			{
			if (find (string, c) != -1)
				{
				return false;
				}
			}

		return true;
		}
	
	bit check_login (char[] user, char[] pass, uint vers, out char[] error)
		{
		if (!db.user_exists (user))
			{
			if (!check_string (user) || user == this.server_user)
				{
				error = "Bad username";
				return false;
				}

			debug (2) writefln ("Adding user ", user, "...");
			db.add_user (user, encode_password (pass));
			return true;
			}
		else
			{
			debug (2) writefln ("User ", user, " is registered, checking banned status and password...");
			if (db.is_banned (user))
				{
				error = "Banned";
				return false;
				}
			else
				{
				char[] real_pass = db.get_pass (user);

				if (real_pass == encode_password (pass) || real_pass == pass)
					{
					return true;
					}
				else
					{
					error = "Bad password";
					return false;
					}
				}
			}
		}
	}

version (linux)
	{
	// sigpipe handling
	extern (C)
		{
		int sigaction (int, sigaction_t*, sigaction_t*);
		alias void (*__sighandler_t)(int);

		static void sigpipe (int sig)
			{
			debug (3) writefln ("Broken pipe");
			}

		int fork ();
		}

	struct sigset_t
		{
		uint __val[1024 / (8 * uint.sizeof)];
		}
		
	struct sigaction_t
		{
		__sighandler_t sa_handler;
		sigset_t sa_mask;
		int sa_flags;
		void (*sa_restorer)();
		}
	}


char[] print_length (long length)
	{
	uint d = length/(60*60*24);
	uint h = length/(60*60) - d*24;
	uint m = length/(60) - d*60*24 - h*60;
	uint s = length - d*60*60*24 - h*60*60 - m*60;
	
	char[] l;
	if (d > 0) l ~= format ("%d %s, ", d, d > 1 ? "days" : "day");
	if (d > 0 || h > 0) l ~= format ("%d %s, ", h, h > 1 ? "hours" : "hour");
	if (d > 0 || h > 0 || m > 0) l ~= format ("%d %s, ", m, m > 1 ? "minutes" : "minute");
	if (d > 0 || h > 0 || m > 0 || s > 0) l ~= format ("%d %s", s, s > 1 ? "seconds" : "second");
	return l;
	}
